﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.IO;
using System.Xml;

using DBMS.Models;
using DBMS.Helpers.Application;


namespace DBMS.Helpers.Metadata
{
    /// <summary>
    /// Manager class for accessing and manipulating metadata files 
    /// associated with databases
    /// </summary>
    public class MetadataManager
    {
        #region Constants

        private const String ELEM_DB = "database";
        private const String ELEM_TABLE = "table";
        private const String ELEM_COUNTER = "counter";

        private const String ATTR_NAME = "name";
        private const String ATTR_ID = "id";
        private const String ATTR_DELETED = "deleted";
        private const String ATTR_LASTINDEX = "lastindex";
        private const String ATTR_LASTROW = "lastrow";

        private const String META_FILE = "metadata.xml";
        private const String INDEX_FILE = "index.xml";

        #endregion

        /// <summary>
        /// Wrapper method for GetMetaFilePath(int)
        /// </summary>
        /// <param name="db">The database object to be used</param>
        /// <returns>The Full path of the meta file</returns>
        /// <see cref="GetMetaFilePath(int)"/>
        public String GetMetaFilePath(Database db)
        {
            return GetMetaFilePath(db.Index);
        }


        /// <summary>
        /// Returns the full path of the metadata file for a database.
        /// </summary>
        /// <param name="databaseId">Id of the database</param>
        /// <returns>The Full path of the meta file</returns>
        public String GetMetaFilePath(int databaseId)
        {
            return Configuration.Instance.WorkingDirectory + "\\" + databaseId.ToString() + "\\" + META_FILE;
        }

        public String GetIndexFilePath(int databaseId, int tableId)
        {
            return Configuration.Instance.WorkingDirectory + "\\" + databaseId.ToString() + "\\" + tableId.ToString() + "\\" + INDEX_FILE;
        }


        /// <summary>
        /// Checks if the metafile for a database exists
        /// </summary>
        /// <param name="databaseId">The id of the database to be checked</param>
        /// <returns>True if the metafile exists, otherwise False</returns>
        public bool DatabaseExists(int databaseId)
        {
            return File.Exists(GetMetaFilePath(databaseId));
        }

        public bool IndexFileExists(int databaseId, int tableId)
        {
            return File.Exists(GetIndexFilePath(databaseId, tableId));
        }


        /// <summary>
        /// Wrapper function for DatabaseExists(int)
        /// </summary>
        /// <param name="db">The database to be checked</param>
        /// <returns>True if the metafile exists, otherwise False</returns>
        /// <see cref="DatabaseExists(int)"/>
        public bool DatabaseExists(Database db)
        {
            return DatabaseExists(db.Index);
        }


        /// <summary>
        /// Extracts the data from the metadata file and builds up a database objects
        /// </summary>
        /// <param name="databaseId">The id of the database to be built</param>
        /// <returns>The database object if found</returns>
        public Database GetDatabase(int databaseId)
        {
            XmlDocument xmlDom = GetMetaFileDom(databaseId);
            XmlNode root = xmlDom.GetElementsByTagName(ELEM_DB).Item(0);
            Database db = new Database();
            db.Name = root.Attributes.GetNamedItem(ATTR_NAME).Value;
            db.Index = Convert.ToInt32(root.Attributes.GetNamedItem(ATTR_ID).Value.ToString());
            db.Deleted = Convert.ToBoolean(root.Attributes.GetNamedItem(ATTR_DELETED).Value.ToString());

            Table table;
            foreach (XmlNode node in xmlDom.GetElementsByTagName(ELEM_TABLE))
            {
                table = GetTableData(node);
                table.Database = db;
                if (!table.Deleted)
                {
                    db.Tables.Add(table);
                }
            }
            return db;
        }

        public Table GetTable(int databaseId, int tableId)
        {
            XmlDocument xmlDom = GetMetaFileDom(databaseId);
            XmlNode root = xmlDom.GetElementsByTagName(ELEM_DB).Item(0);

            Database db = new Database();
            db.Name = root.Attributes.GetNamedItem(ATTR_NAME).Value;
            db.Index = Convert.ToInt32(root.Attributes.GetNamedItem(ATTR_ID).Value.ToString());
            db.Deleted = Convert.ToBoolean(root.Attributes.GetNamedItem(ATTR_DELETED).Value.ToString());

            Table table = null;
            foreach (XmlNode node in xmlDom.GetElementsByTagName(ELEM_TABLE))
            {
                table = GetTableData(node);
                if (!table.Deleted)
                {
                    //db.Tables.Add(table);
                    if (table.Index == tableId)
                    {
                        return table;
                    }
                }
            }

            return table;
        }

        public int GetLastIndexForTable(int databaseId, int tableId)
        {
            XmlDocument xmlDom = GetIndexFileDom(databaseId, tableId);
            XmlNode root = xmlDom.GetElementsByTagName(ELEM_COUNTER).Item(0);
            return Int32.Parse(root.Attributes.GetNamedItem(ATTR_LASTINDEX).Value);
        }

        public void SetCountersForTable(int index, int row, int databaseId, int tableId)
        {
            String dbMetaFile = GetIndexFilePath(databaseId, tableId);
            XmlWriterSettings settings = new XmlWriterSettings();
            settings.Indent = true;
            settings.Encoding = System.Text.Encoding.UTF8;
            settings.CloseOutput = true;

            using (XmlWriter writer = XmlWriter.Create(dbMetaFile, settings))
            {
                writer.WriteStartDocument();
                writer.WriteStartElement(ELEM_COUNTER);
                writer.WriteAttributeString(ATTR_LASTINDEX, index.ToString());
                writer.WriteAttributeString(ATTR_LASTROW, row.ToString());
                writer.WriteEndElement();
                writer.WriteEndDocument();
                writer.Flush();
                writer.Close();
            }
        }

        public void SetLastIndexForTable(int index, int databaseId, int tableId)
        {
            String dbMetaFile = GetIndexFilePath(databaseId, tableId);
            XmlWriterSettings settings = new XmlWriterSettings();
            settings.Indent = true;
            settings.Encoding = System.Text.Encoding.UTF8;
            settings.CloseOutput = true;

            using (XmlWriter writer = XmlWriter.Create(dbMetaFile, settings))
            {
                writer.WriteStartDocument();
                writer.WriteStartElement(ELEM_COUNTER);
                writer.WriteAttributeString(ATTR_LASTINDEX, index.ToString());
                writer.WriteEndElement();
                writer.WriteEndDocument();
                writer.Flush();
                writer.Close();
            }
        }

        public int GetLastRowForTable(int databaseId, int tableId)
        {
            XmlDocument xmlDom = GetIndexFileDom(databaseId, tableId);
            XmlNode root = xmlDom.GetElementsByTagName(ELEM_COUNTER).Item(0);
            return Int32.Parse(root.Attributes.GetNamedItem(ATTR_LASTROW).Value);
        }

        public void SetLastRowForTable(int row, int databaseId, int tableId)
        {
            String dbMetaFile = GetIndexFilePath(databaseId, tableId);
            XmlWriterSettings settings = new XmlWriterSettings();
            settings.Indent = true;
            settings.Encoding = System.Text.Encoding.UTF8;
            settings.CloseOutput = true;

            using (XmlWriter writer = XmlWriter.Create(dbMetaFile, settings))
            {
                writer.WriteStartDocument();
                writer.WriteStartElement(ELEM_COUNTER);
                writer.WriteAttributeString(ATTR_LASTROW, row.ToString());
                writer.WriteEndElement();
                writer.WriteEndDocument();
                writer.Flush();
                writer.Close();
            }
        }


        /// <summary>
        /// Creates a metadata file for the database
        /// </summary>
        /// <param name="database">The object containing the information associated with the database</param>
        /// <exception cref="Exception">If the metafile for the database already exists</exception>
        /// <returns>True if the meta file was created, otherwise false</returns>
        public bool CreateDatabase(Database database)
        {
            if (DatabaseExists(database))
            {
                throw new Exception("The database " + database.Name + " already exists");
            }

            String dbMetaFile = GetMetaFilePath(database);
            XmlWriterSettings settings = new XmlWriterSettings();
            settings.Indent = true;
            settings.Encoding = System.Text.Encoding.UTF8;
            settings.CloseOutput = true;
            
            using ( XmlWriter writer = XmlWriter.Create(dbMetaFile, settings) )
            {
                writer.WriteStartDocument();
                writer.WriteStartElement(ELEM_DB);
                writer.WriteAttributeString(ATTR_NAME, database.Name);
                writer.WriteAttributeString(ATTR_ID, database.Index.ToString() );
                writer.WriteAttributeString(ATTR_DELETED, database.Deleted.ToString());
                writer.WriteEndElement();
                writer.WriteEndDocument();
                writer.Flush();
                writer.Close();
            }

            return true;
        }

        public void UpdateDatabase(Database database)
        {
            String metaFilePath = Configuration.Instance.WorkingDirectory + "\\" + database.Index.ToString() + "\\" + META_FILE;
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.Load(metaFilePath);
            XmlNode root = xmlDoc.GetElementsByTagName(ELEM_DB).Item(0);
            root.Attributes.GetNamedItem(ATTR_NAME).Value = database.Name;
            xmlDoc.Save(metaFilePath);
        }


        /// <summary>
        /// Sets the deleted attribute of the database to true
        /// </summary>
        /// <param name="databaseId">The id of the database used</param>
        public void DeleteDatabase(int databaseId)
        {
            XmlDocument xmlDom = GetMetaFileDom(databaseId);
            XmlNode root = xmlDom.GetElementsByTagName(ELEM_DB).Item(0);

            root.Attributes.GetNamedItem(ATTR_DELETED).Value = true.ToString();
        }


        /// <summary>
        /// Deletes the metadata file of the database
        /// </summary>
        /// <param name="databaseId">The id of the database to be deleted</param>
        public void DeleteDatabasePhysical(int databaseId)
        {
            File.Delete(GetMetaFilePath(databaseId));
        }


        /// <summary>
        /// Adds the table with the attributes to the xml file of the database
        /// </summary>
        /// <param name="database">The database that will contain the table</param>
        /// <param name="table">The table object to be saved into the xml file</param>
        /// <returns>True if the operation was successful</returns>
        public bool AddTable(Database database, Table table)
        {
            XmlDocument xmlDom = GetMetaFileDom(database);
            XmlNodeList rootElements = xmlDom.GetElementsByTagName(ELEM_DB);

            XmlNode databaseNode = rootElements.Item(0);
            XmlNode tableNode = xmlDom.CreateElement(ELEM_TABLE);
            tableNode.Attributes.Append(GenerateAttribute(ATTR_ID, Convert.ToString(table.Index), xmlDom));
            tableNode.Attributes.Append(GenerateAttribute(ATTR_NAME, table.Name, xmlDom));
            tableNode.Attributes.Append(GenerateAttribute(ATTR_DELETED, false.ToString(), xmlDom));
            databaseNode.AppendChild(tableNode);

            SaveXmlDomToFile(xmlDom, database);

            CreateIndexForTable(database, table);

            return true;
        }

        public void CreateIndexForTable(Database database, Table table)
        {
            String dbMetaFile = Configuration.Instance.WorkingDirectory + "\\" + database.Index.ToString() + "\\" + table.Index.ToString() + "\\" + INDEX_FILE;
            XmlWriterSettings settings = new XmlWriterSettings();
            settings.Indent = true;
            settings.Encoding = System.Text.Encoding.UTF8;
            settings.CloseOutput = true;

            using (XmlWriter writer = XmlWriter.Create(dbMetaFile, settings))
            {
                writer.WriteStartDocument();
                writer.WriteStartElement(ELEM_COUNTER);
                writer.WriteAttributeString(ATTR_LASTINDEX, "0");
                writer.WriteAttributeString(ATTR_LASTROW, "0");
                writer.WriteEndElement();
                writer.WriteEndDocument();
                writer.Flush();
                writer.Close();
            }
        }

        public void DeleteTable(int databaseId, int tableId)
        {
            Database db = GetDatabase(databaseId);

            XmlDocument xmlDom = GetMetaFileDom(db);
            XmlNodeList tables = xmlDom.GetElementsByTagName(ELEM_TABLE);

            foreach (XmlNode table in tables)
            {
                if (Convert.ToInt32(table.Attributes.GetNamedItem(ATTR_ID).Value.ToString()) == tableId)
                {
                    table.Attributes.GetNamedItem(ATTR_DELETED).Value = true.ToString();
                }
            }

            SaveXmlDomToFile(xmlDom, db);
        }

        public void UpdateTable(Database database, Table newTable)
        {
            String metaFilePath = Configuration.Instance.WorkingDirectory + "\\" + database.Index.ToString() + "\\" + META_FILE;
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.Load(metaFilePath);

            XmlNodeList tables = xmlDoc.GetElementsByTagName(ELEM_TABLE);

            foreach (XmlNode table in tables)
            {
                if (Convert.ToInt32(table.Attributes.GetNamedItem(ATTR_ID).Value.ToString()) == newTable.Index)
                {
                    table.Attributes.GetNamedItem(ATTR_NAME).Value = newTable.Name;
                }
            }

            xmlDoc.Save(metaFilePath);
        }


        /// <summary>
        /// Generates an xml attribute object
        /// </summary>
        /// <example><![CDATA[<xml key="value" />]]></example>
        /// <param name="key">The key of the attribute</param>
        /// <param name="value">Value associated with the kez</param>
        /// <param name="xmlDom">The dom object used to generate the attribute</param>
        /// <returns>The XmlAttribute generated</returns>
        private XmlAttribute GenerateAttribute(String key, String value, XmlDocument xmlDom)
        {
            XmlAttribute attribute = xmlDom.CreateAttribute(key);
            attribute.Value = value;
            return attribute;
        }


        /// <summary>
        /// Gets the full path of the metafile associated with the database and
        /// returns an XmlDocument filled up with xml data
        /// </summary>
        /// <param name="databaseId">The id of the database used</param>
        /// <returns>The object filled with xml data</returns>
        private XmlDocument GetMetaFileDom(int databaseId)
        {
            return GetMetaFileDom(GetMetaFilePath(databaseId));
        }

        private XmlDocument GetIndexFileDom(int databaseId, int tableId)
        {
            return GetIndexFileDom(GetIndexFilePath(databaseId, tableId));
        }

        /// <summary>Wrapper function for GetMetaFileDom(int)</summary>
        /// <see cref="GetMetaFileDom(int)"/>
        private XmlDocument GetMetaFileDom(Database db)
        {
            return GetMetaFileDom(db.Index);
        }


        /// <summary>
        /// Loads the metadata xml file into an XmlDocument object
        /// </summary>
        /// <param name="indexFilePath">The full path of the xml metafile</param>
        /// <returns>The object filled up with metadata</returns>
        private XmlDocument GetIndexFileDom(string indexFilePath)
        {
            FileStream input = new FileStream(indexFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
            XmlDocument xmlDom = new XmlDocument();
            xmlDom.Load(input);
            input.Close();
            return xmlDom;
        }

        private XmlDocument GetMetaFileDom(string metaFilePath)
        {
            FileStream input = new FileStream(metaFilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
            XmlDocument xmlDom = new XmlDocument();
            xmlDom.Load(input);
            input.Close();

            XmlNodeList rootElements = xmlDom.GetElementsByTagName(ELEM_DB);
            if (rootElements.Count != 1)
            {
                throw new Exception("Badly formatted xml. More than 1 root elements found");
            }

            return xmlDom;
        }


        private Table GetTableData(XmlNode node)
        {
            Table table = new Table();
            table.Name = node.Attributes.GetNamedItem(ATTR_NAME).Value.ToString();
            table.Index = Convert.ToInt32(node.Attributes.GetNamedItem(ATTR_ID).Value.ToString());
            table.Deleted = Convert.ToBoolean(node.Attributes.GetNamedItem(ATTR_DELETED).Value.ToString());
            return table;
        }

        private void SaveXmlDomToFile(XmlDocument xmlDom, Database db)
        {
            FileStream output = new FileStream(GetMetaFilePath(db), FileMode.Truncate,
                   FileAccess.Write, FileShare.ReadWrite);
            xmlDom.Save(output);
            output.Close();
        }
    }
}